package ua.org.enginer.maps {
	
	import flash.display.DisplayObject;
	import flash.events.MouseEvent;
	import flash.geom.Point;
	import flash.utils.Dictionary;
	
	import mx.containers.Canvas;
	import mx.controls.Image;
	import mx.controls.Text;
	import mx.events.ItemClickEvent;
	import mx.events.SliderEvent;
	
	[Event(name="zoomChange", type="ua.org.enginer.maps.MapEvent")]
	
	[Style(name="tileStyleName", type="String", inherit="no")]

	// logo http://api-maps.yandex.ru/i/0.2/YMaps-logo.png
	// © ЗАО «ТГА», 2006, © ВТУ ГШ ВС РФ, © ЗАО «Резидент», © НДIГК
	//  Условия использования http://maps.yandex.ru/agreement.xml
	// http://img.yandex.net/maps/i/404.gif
	/**
	 * Алгорит работы карты в целом не плохо описан сдесь http://mapbuilder.narod.ru/gm.htm.
	 * Многие переменные названы аналогично.
	 * 
	 * @author Lebid Yuriy ylebid@gmail.com 
	 * 2009
	 */
	public class YandexMap extends Canvas implements IMap {
		
		public static const MAX_ZOOM:int = 17
		
		/**
		 * layers
		 */ 
		private var tailsLayer:Canvas
		private var markersLayer:Canvas
		private var controlsLayer:Canvas
		
		
		private var dragStarted:Boolean
		public var mapDrawn:Boolean
		private var needRedrawMap:Boolean = true
		
		/**
		 * Кеш фрагментов карты
		 */ 
		private var tiles:Dictionary /* of String "type:x|y" */ = new Dictionary()
		
		/**
		 * Центральный фрагмент карты
		 */ 
		private var centerTile:Point
		
		public function YandexMap() {
			super();
			horizontalScrollPolicy = "off"
			verticalScrollPolicy= "off"
			
			tailsLayer = new Canvas()
			// создаю сдесь, чтобы можно было сразу добавлять маркеры
			markersLayer = new Canvas()
			
			markersLayer.clipContent = false
			
			markersLayer.horizontalScrollPolicy = "off"
			markersLayer.verticalScrollPolicy = "off"
			
			// и контролы )
			controlsLayer = new Canvas()
			
			addEventListener(MouseEvent.MOUSE_WHEEL, onWheel)
		}
		
		protected function onWheel(event:MouseEvent):void {
			// чтобы не сильно резко менять зум делим шаг на 3
			zoom += Math.round(event.delta/3)
		}
		
		/**
		 * Уровень детализации карты (zoom)
		 */ 
		private var _zoom:int = 10;
		[Bindable]
		public function get zoom():int {
			return _zoom;
		}
		public function set zoom(value:int):void {
			if (value<0)
				value = 0
			if (value>MAX_ZOOM)
				value = MAX_ZOOM

			_zoom = value;
			
			needRedrawMap = true
			invalidateDisplayList()
			
			dispatchEvent(new MapEvent(MapEvent.Z_CHANGE))
			if (zoomControl) zoomControl.value = zoom
			
			repositionMarkers()
		}

		protected function moveMap(x:Number, y:Number):void {
			tailsLayer.move(x, y)
			markersLayer.move(x, y)
			fillByTiles(-x, -y, width, height)
		}
		
		override protected function createChildren():void {
			super.createChildren()
			
			addChild(tailsLayer)
			tailsLayer.useHandCursor = true
			tailsLayer.buttonMode = true
			tailsLayer.clipContent = false
			tailsLayer.styleName = this
			
			tailsLayer.addEventListener(MouseEvent.MOUSE_DOWN, startMapDrag)
			addChild(markersLayer)
			
			// для правильного позиционирования контролов привязанных к правой стороне
			controlsLayer.percentHeight = 100
			controlsLayer.percentWidth = 100
			addChild(controlsLayer)
			
			addLogoAndCopyRights()
		}
		
		override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number) : void {
			super.updateDisplayList(unscaledWidth, unscaledHeight)
			
			if (needRedrawMap || !mapDrawn) {
				needRedrawMap = false
				clearMap()
				drawMap()
			}
		}
		
		private function startMapDrag(event:MouseEvent):void {
			systemManager.addEventListener(MouseEvent.MOUSE_UP, stopMapDrag)
			systemManager.addEventListener(MouseEvent.MOUSE_MOVE, onMapMove)
			
			tailsLayer.startDrag()
			dragStarted = true
		}
		
		private function onMapMove(event:MouseEvent):void {
			if (!dragStarted) return;
			// привязываем маркеры к перетаскиванию мышой карты
			markersLayer.x = tailsLayer.x
			markersLayer.y = tailsLayer.y
		}
		
		private function stopMapDrag(event:MouseEvent):void {
			if (!dragStarted) return;
			dragStarted = false
			
			systemManager.removeEventListener(MouseEvent.MOUSE_UP, stopMapDrag)
			systemManager.removeEventListener(MouseEvent.MOUSE_MOVE, onMapMove)
			
			tailsLayer.stopDrag()
			fillByTiles(-tailsLayer.x, -tailsLayer.y, width, height)
			
			// вычисляем цент карты в географических координатах
			var centerPoint:Point = new Point(width/2, height/2)
			centerPoint = localToGlobal(centerPoint)
			centerPoint = tailsLayer.globalToLocal(centerPoint)
			
			_center = getCoordinatesFromPoint(centerPoint.x, centerPoint.y)
		}
		
		
		/**
		 * выполняеться при изменнии зума
		 */ 
		protected function repositionMarkers():void {
			var marker:IMapMarker
			var markerNum:int = markersLayer.numChildren
			var p:Point
			for (var i:int=0; i<markerNum; i++) {
				marker = IMapMarker(markersLayer.getChildAt(i))
				
				p = getPointFromCoordinates(marker.latitude, marker.longitude)
			
				marker.x = p.x - centerTile.x*tileSize
				marker.y = p.y - centerTile.y*tileSize
			} 
		}
		
		/**
		 * Тип карты 
		 */ 
		private var _type:String = MapTypes.NORMAL//"hybrid"//"map";
		public function set type(value:String):void {
			_type = value;
			needRedrawMap = true
			invalidateDisplayList()
		}

		public function get type():String {
			return _type;
		}
		
		/**
		 * Версия карт для каждого типа.
		 * Нужно периодически обновлять.
		 * Для метода getTileUrl
		 */ 
		protected function getVersion(type:String):String {
			switch(type) {
				case "map":
					return "2.6.0"
				case "skl":
					return "2.6.0"
				case "sat":
					return "1.9.0"
				default:
					return ""
			}
		}
		
		/**
		 * Поддомен хранилища для каждого типа.
		 * Для метода getTileUrl
		 */ 
		protected function getSubdomain(type:String):String {
			switch(type) {
				case "map":
					return "vec"
				case "sat":
					return "sat"
				case "skl":
					return "vec"
				default:
					return ""
			}
		}
		
		/**
		 * Переписывая этот метод можно "обучить" другим картам
		 */ 
		protected function getTileUrl(x:int, y:int, z:int, type:String):String {
			return "http://"+getSubdomain(type)+"01.maps.yandex.ru/tiles?l="+type+"&v="+getVersion(type)+"&x="+x+"&y="+y+"&z="+z
		}
		
		/**
		 * Географические координаты центра. По умолчаниию Киев
		 */
		private var _center:GeoPoint = new GeoPoint(50.448445, 30.451918);
		public function set center(value:GeoPoint):void {
			_center = value;
			if (!mapDrawn) return;
			
			var p:Point = getPointFromCoordinates(value.latitude, value.longitude)
			
			moveMap(width/2 - p.x+centerTile.x*tileSize, height/2 - p.y+centerTile.y*tileSize)
		}
		
		[Bindable]
		public function get center():GeoPoint {
			return _center;
		}
		
		public function clearMap():void {
			tailsLayer.removeAllChildren()
			tiles = new Dictionary()
		}
		
		/**
		 * Точка внутри центрального фрагмента
		 */ 
		private var inBlockPoint:Point
		
		public function drawMap():void {
			var p:Point = getPointFromCoordinates(center.latitude, center.longitude)
			centerTile = new Point(Math.floor(p.x/tileSize), Math.floor(p.y/tileSize))
			
			inBlockPoint = new Point(p.x - centerTile.x*tileSize, p.y - centerTile.y*tileSize)
			fillByTiles(0, 0, width, height)
			
			tailsLayer.move(width/2-inBlockPoint.x, height/2-inBlockPoint.y)
			markersLayer.move(tailsLayer.x, tailsLayer.y)
			
			repositionMarkers()

			mapDrawn = true
		}
		
		
		/**
		 * Заполнение видимого пространства фрагментами карты
		 */ 
		private function fillByTiles(startX:Number, startY:Number, width:Number, height:Number):void {
			var horizontalBlocksNum:int = Math.ceil(width/tileSize)
			var verticalBlocksNum:int = Math.ceil(height/tileSize)
			
			var xmax:Number = Math.ceil(horizontalBlocksNum/2)+Math.ceil(startX/tileSize)
			var ymax:Number = Math.ceil(verticalBlocksNum/2)+Math.ceil(startY/tileSize)
			
			// флгирит заполнения фрагментами расчитан на квадратное окно просмотра...
			// для прямоугольно со сдвигом центральной точки нужну ограничения по сторонам
			var left:int = Math.ceil((width/2 - inBlockPoint.x)/tileSize)
			var right:int = Math.floor((width/2 + inBlockPoint.x)/tileSize)
			var top:int = Math.ceil((height/2 - inBlockPoint.y)/tileSize)
			var bottom:int = Math.floor((height/2 + inBlockPoint.y)/tileSize)
			
			
			var i:int
			
			// приобразование координат сдвига на карте в координаты 
			// сдвига порядковых номеров фрагментов карты
			startX = Math.ceil(startX/tileSize)
			startY = Math.ceil(startY/tileSize)
			if (startX) startX ++
			if (startY) startY ++
			
			
			// заполнение фрагментами по спирали
			addTile(startX, startY)
			// level - уровень спирали
			var maxLevel:int = Math.max(Math.max(top, right),Math.max(top, bottom))
			for (var level:int=1; level<=maxLevel; level++)
			{
				if (level<=left)
					for(i=level-1; i>=-level; i--)
						addTile(-level+startX, i+startY)
				if (level<=top)
					for(i=1-level; i<=level; i++)
						addTile(i+startX, -level+startY)
				if (level<=right)
					for(i=1-level; i<=level; i++)
						addTile(level+startX, i+startY)
				if (level<=bottom)
					for(i=level-1; i>=-level; i--)
						addTile(i+startX, level+startY)
			}
		}
		
		/**
		 * @param Горизонтальный порядковый номер фрагмента
		 * @param Вертикальный порядковый номер фрагмента
		 */ 
		protected function addTile(x:Number, y:Number, type:String=''):void {
			if (tiles[type+":"+x+"|"+y]) return;
			trace("draw tile",x,y)
			if (type=='') type = this.type
			
			var img:Image = new Image()
			img.styleName = getStyle("tileStyleName")
			img.source = getTileUrl(centerTile.x+x, centerTile.y+y, zoom, type)
			img.x = x*tileSize
			img.y = y*tileSize 
			tailsLayer.addChild(img)
			
			// Чтобы повторно не создавать фрагмент при перетаскивании
			tiles[type+":"+x+"|"+y] = img
		}
		
		private const tileSize:int = 256; //px
		
		private function get numTiles():int {
			return Math.pow(2, zoom)
		}
		
		private function get bitmapSize():Number {
			return numTiles*tileSize
		}
		
		private function get pixelsPerLonDegree():Number {
			return bitmapSize/360
		}
		
		private function get pixelsPerLonRadian():Number {
			return bitmapSize/(2*Math.PI)
		}
		
		private function get bitmapCenter():Number {
			return bitmapSize/2
		}
		
		/**
		 * Алгорит оказался не идеальным (. 
		 * Пришлось вводить поправку по Y
		 */ 
		private function get correctionY():Number {
			return numTiles/4.75
		}
		
		private function getPointFromCoordinates(lat:Number, long:Number):Point {
			var point:Point = new Point()
			point.x = Math.round(bitmapCenter + long*pixelsPerLonDegree)
			
			var z:Number = Math.sin(lat*Math.PI/180)
			var c:Number = bitmapCenter/Math.PI
			
			
			point.y = bitmapCenter - 0.5*Math.log((1+z)/(1-z))*pixelsPerLonRadian
			point.y += correctionY
			point.y = Math.round(point.y)
			
			return point
		}
		
		
		private function getCoordinatesFromPoint(x:Number, y:Number):GeoPoint {
			x = centerTile.x*tileSize + x
			y = centerTile.y*tileSize + y

			y -= correctionY
			
			var mpoint:GeoPoint = new GeoPoint()
			mpoint.longitude = (x - bitmapCenter)/pixelsPerLonDegree;
			mpoint.latitude = (2*Math.atan(Math.exp(-(y-bitmapCenter)/pixelsPerLonRadian)) - Math.PI/2)*180/Math.PI;
			
			return mpoint
		}
		
		
		
		public function addMarker(marker:IMapMarker, latitude:Number, longitude:Number):void {
			
			marker.latitude = latitude
			marker.longitude = longitude
			marker.map = this
			
			marker.addEventListener(MouseEvent.MOUSE_DOWN, startMapDrag)
			
			if (centerTile) {
				var p:Point = getPointFromCoordinates(latitude, longitude)
				
				marker.x = p.x - centerTile.x*tileSize
				marker.y = p.y - centerTile.y*tileSize
			}
			
			markersLayer.addChild(DisplayObject(marker))
		}
		
		private var zoomControl:ZoomControl
		
		public function addZoomControl():void {
			zoomControl = new ZoomControl()
			zoomControl.value = zoom
			zoomControl.minimum = 0
			zoomControl.maximum = MAX_ZOOM
			
			controlsLayer.addChild(zoomControl)
			zoomControl.addEventListener(SliderEvent.CHANGE, function(event:SliderEvent):void{zoom=event.target.value})
		}
		
		public function addCenterCross():void {
			var centr:Point = new Point(width/2, height/2)
			var crossRadius:Number = 10
			
			controlsLayer.graphics.lineStyle(2, 0xFF0000)
			controlsLayer.graphics.moveTo(centr.x - crossRadius, centr.y)
			controlsLayer.graphics.lineTo(centr.x + crossRadius, centr.y)
			controlsLayer.graphics.moveTo(centr.x, centr.y - crossRadius)
			controlsLayer.graphics.lineTo(centr.x, centr.y + crossRadius)
		}
		
		private var typeControl:TypeControl
		
		public function addTypeControl():void {
			typeControl = new TypeControl()
			typeControl.selectedType = type
			typeControl.addEventListener(ItemClickEvent.ITEM_CLICK, function(event:ItemClickEvent):void {
				type = event.target.dataProvider[event.target.selectedIndex].data
			})
			controlsLayer.addChild(typeControl)
		}
		
		//todo: Надобы менять цвет текста и лого при смене типа карт, а то на сателите оч плохо видно
		protected function addLogoAndCopyRights():void {
			var logo:Image = new Image()
			logo.source = "http://api-maps.yandex.ru/i/0.2/YMaps-logo.png"
			logo.setStyle("bottom", 0)
			logo.setStyle("right", 10)
			controlsLayer.addChild(logo)
			
			var useConditions:Text = new Text()
			useConditions.htmlText = "© European Space Imaging GmBH, © ООО ИТЦ «СКАНЭКС», © НДIГК, © ЗАО «Резидент» - " + 
					"<a traget='_blank' href='http://maps.yandex.ru/agreement.xml'>Условия использования</a>"
			useConditions.setStyle("bottom", 0)
			useConditions.setStyle("right", 10)
			controlsLayer.addChild(useConditions)
		}
		
		/**
		 * metrical scale
		 * metres in pixel
		 */ 
		public function get scale():Number {
			return 80*Math.pow(2, 10-zoom);
		}
	}
}

